ToolLoopAgent

abstract class ToolLoopAgent<TContext, TOutput>(val model: LanguageModel, val instructions: String, val tools: ToolSet<TContext>, val activeTools: List<String>? = null, val output: Output<TOutput>? = null, val stopWhen: StopCondition = stepCountIs(20), val prepareCall: suspend PrepareCallScope<TContext>.() -> AgentSettings<TContext>? = null, val prepareStep: suspend PrepareStepScope<TContext>.() -> StepSettings<TContext>? = null, val callOptionsSchema: KSerializer<TContext>? = null, val temperature: Float? = null, val topP: Float? = null, val topK: Int? = null, val maxOutputTokens: Int? = null, val stopSequences: List<String>? = null, val seed: Int? = null, val presencePenalty: Float? = null, val frequencyPenalty: Float? = null, val responseFormat: ResponseFormat = ResponseFormat.Text, val maxParallelToolCalls: Int = Int.MAX_VALUE, val onStart: suspend OnStartEvent.() -> Unit? = null, val onStepStart: suspend OnStepStartEvent.() -> Unit? = null, val onStepFinish: suspend OnStepFinishEvent.() -> Unit? = null, val onFinish: suspend OnFinishEvent.() -> Unit? = null, val onError: suspend OnErrorEvent.() -> Unit? = null, val onChunk: suspend OnChunkEvent.() -> Unit? = null, val onAbort: suspend OnAbortEvent.() -> Unit? = null, val experimental_onToolCallStart: suspend OnToolCallStartEvent.() -> Unit? = null, val experimental_onToolCallFinish: suspend OnToolCallFinishEvent.() -> Unit? = null, val experimental_repairToolCall: ToolCallRepairFunction<TContext>? = null, val experimental_toolApprovalSecret: ByteArray? = null, val telemetry: TelemetrySettings? = null, val logger: Logger = NoopLogger, engineContext: CoroutineContext = Dispatchers.Default) : Agent<TContext, TOutput>

Abstract base Agent implementation — the canonical v6 ToolLoopAgent. Extend it; do not instantiate it. A concrete agent subclasses this (e.g. class MyAgent(...) : ToolLoopAgent<C, O>(...)) so the agent's identity, tools, lifecycle overrides, and DI live in one named type — never a bare ToolLoopAgent(...) held as a field.

Runs the model, executes tool calls, accumulates the message list, checks stopWhen after each step, surfaces lifecycle events. Manages the loop end-to-end (invariant I-7) — no manual loop in user code.

Per the v6 source comment in packages/ai/src/agent/tool-loop-agent.ts, the loop continues until:

  • A finish reason other than tool-calls is returned, OR

  • A tool that is invoked does not have an execute function, OR

  • A tool call needs approval, OR

  • A stop condition is met (default stepCountIs(20)).

Approval flow (v6 RPC semantics)

When a tool's needsApproval(input, context) returns true:

  1. Emit StreamEvent.ToolApprovalRequest in the stream.

  2. Append ContentPart.ToolApprovalRequest to the assistant message.

  3. End the loop — return control to the host with GenerateResult.pendingApprovals populated.

  4. Host calls generate again with messages = result.messages + toolApprovalResponseMessage(...).

  5. Loop resumes; approved tools execute; denied tools are skipped and the denial reason flows back to the model as a tool result.

Generation isn't kept "in flight" while the user decides — host can serialize, persist, transport, then resume.

Constructors

Link copied to clipboard
constructor(model: LanguageModel, instructions: String, tools: ToolSet<TContext>, activeTools: List<String>? = null, output: Output<TOutput>? = null, stopWhen: StopCondition = stepCountIs(20), prepareCall: suspend PrepareCallScope<TContext>.() -> AgentSettings<TContext>? = null, prepareStep: suspend PrepareStepScope<TContext>.() -> StepSettings<TContext>? = null, callOptionsSchema: KSerializer<TContext>? = null, temperature: Float? = null, topP: Float? = null, topK: Int? = null, maxOutputTokens: Int? = null, stopSequences: List<String>? = null, seed: Int? = null, presencePenalty: Float? = null, frequencyPenalty: Float? = null, responseFormat: ResponseFormat = ResponseFormat.Text, maxParallelToolCalls: Int = Int.MAX_VALUE, onStart: suspend OnStartEvent.() -> Unit? = null, onStepStart: suspend OnStepStartEvent.() -> Unit? = null, onStepFinish: suspend OnStepFinishEvent.() -> Unit? = null, onFinish: suspend OnFinishEvent.() -> Unit? = null, onError: suspend OnErrorEvent.() -> Unit? = null, onChunk: suspend OnChunkEvent.() -> Unit? = null, onAbort: suspend OnAbortEvent.() -> Unit? = null, experimental_onToolCallStart: suspend OnToolCallStartEvent.() -> Unit? = null, experimental_onToolCallFinish: suspend OnToolCallFinishEvent.() -> Unit? = null, experimental_repairToolCall: ToolCallRepairFunction<TContext>? = null, experimental_toolApprovalSecret: ByteArray? = null, telemetry: TelemetrySettings? = null, logger: Logger = NoopLogger, engineContext: CoroutineContext = Dispatchers.Default)

Properties

Link copied to clipboard
Link copied to clipboard
val callOptionsSchema: KSerializer<TContext>?
Link copied to clipboard

Port-level engine state machine (isStreaming, currentToolCalls, pendingApprovals, totalSteps, etc.). Distinct from the app-level host-specific orchestration state which is a host-specific reactive lifecycle surface layered on top. Renamed from state so subclasses can declare their own state member without member-hiding clashes.

Link copied to clipboard

Self-healing callback fired when a tool call's arguments fail to decode. Return a corrected call to retry, or null to surface StreamEvent.ToolError. See ToolCallRepairFunction.

Link copied to clipboard

Secret for HMAC-signing tool-approval requests (upstream v6.0.202 experimental_toolApprovalSecret). When set, every approval request the loop issues carries a signature over (approvalId, toolCallId, toolName, input), and a replayed approval is re-validated FAIL-CLOSED before the tool executes: missing/invalid signature throws AgentError.InvalidToolApprovalSignature, the input is re-validated against the tool's schema, and a tool that no longer requires approval is denied rather than run — so a client cannot forge, re-target, or input-swap an approval. Experimental upstream (can break in patches).

Link copied to clipboard

v6 CallSettings.frequencyPenalty agent-default.

Link copied to clipboard
open val id: String

Stable identifier for the agent, useful for telemetry, log routing, and the Agent.version / id / tools accessors v6 exposes (per historical parity gap #33). Implementations default to "agent"; override in the constructor or via class member to set a more specific tag ("chat-agent", "lineup-subagent", etc.).

Link copied to clipboard
Link copied to clipboard

Port-side log sink for non-fatal warnings (see Logger). The loop warns here when a Telemetry integration throws and the event is dropped — the swallow contract keeps telemetry from altering the loop, and the warn keeps a broken integration DISCOVERABLE instead of perfectly silent.

Link copied to clipboard
Link copied to clipboard

Max tool calls executed concurrently within one step. Default unbounded — all of a step's tool calls run at once (latency = slowest tool, not the sum). Results are still applied to the message log in deterministic call order.

Link copied to clipboard
Link copied to clipboard
val onAbort: suspend OnAbortEvent.() -> Unit?
Link copied to clipboard
val onChunk: suspend OnChunkEvent.() -> Unit?
Link copied to clipboard
val onError: suspend OnErrorEvent.() -> Unit?
Link copied to clipboard
val onFinish: suspend OnFinishEvent.() -> Unit?
Link copied to clipboard
val onStart: suspend OnStartEvent.() -> Unit?
Link copied to clipboard
val onStepFinish: suspend OnStepFinishEvent.() -> Unit?
Link copied to clipboard
val onStepStart: suspend OnStepStartEvent.() -> Unit?
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

v6 CallSettings.presencePenalty agent-default.

Link copied to clipboard

Wire-level response constraint for providers that support it.

Link copied to clipboard
val seed: Int?
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

Telemetry for this agent's invocations (upstream v7 telemetry). Telemetry integrations registered globally via registerTelemetry observe every generate/stream call automatically; this setting adds call metadata (TelemetrySettings.functionId) or per-agent TelemetrySettings.integrations — which, when non-empty, REPLACE the global registrations for this agent's calls (upstream per-call semantics). Telemetry observes — an integration throw never alters loop behavior.

Link copied to clipboard
Link copied to clipboard
open override val tools: ToolSet<TContext>

The tool surface this agent carries. Exposed at the interface so consumers can inspect / dispatch without casting to ToolLoopAgent.

Link copied to clipboard
val topK: Int?
Link copied to clipboard
val topP: Float?
Link copied to clipboard
open val version: String?

Optional version string — "1.0.0", a git sha, anything stable across an app build. Null = unversioned. Lets telemetry pin issues to a specific agent revision.

Functions

Link copied to clipboard
fun close()

Cancel the engine scope and any in-flight engine job. Call when a long-lived host (ViewModel / Repository) that drove the engine surface is disposed. The per-call generate()/stream() API needs no close().

Link copied to clipboard

Drive the engine. Each action either advances engineState or aborts the in-flight stream. Idempotent — calling ToolLoopAgentAction.Cancel when nothing is streaming is a no-op.

Link copied to clipboard
open suspend override fun generate(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever, hooks: AgentCallHooks? = null): GenerateResult<TOutput>

One-shot generation. Either prompt or messages (or both) must be supplied. When both are present, prompt is appended as a trailing user message.

Link copied to clipboard
fun <TContext, TOutput> Agent<TContext, TOutput>.session(scope: CoroutineScope, initialMessages: List<ModelMessage> = emptyList()): AgentSession<TContext, TOutput>
Link copied to clipboard
open override fun stream(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever, hooks: AgentCallHooks? = null): Flow<StreamEvent>

Streaming generation. Cold flow — starts when collected.